using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Web;
using LitJson;

namespace SynoManager.Communication
{
    public abstract class StationClientBase
    {
        private const string loginScriptLocation = @"/webman/modules/login.cgi";
        private readonly string serviceLocation;

        protected readonly SynoDevice device;
        private readonly int serviceID;
        protected Uri baseUri;

        protected static CookieContainer cookieContainer;

        protected StationClientBase(SynoDevice device, int serviceID, string location)
        {
            this.device = device;
            this.serviceID = serviceID;

            bool secure = device.PreferSecure;

            if (secure)
                AcceptAllCertificatesPolicy.Install();

            string urlBase = String.Format(
                "{0}://{1}:{2}",
                (secure) ? "https" : "http",
                device.Address,
                (secure) ? device.SecurePort : device.Port);
            try
            {
                baseUri = new Uri(urlBase);
            }
            catch (UriFormatException ex)
            {
                throw new CommunicationException("Invalid location " + device.Address, ex);
            }
            this.serviceLocation = location;
        }

        public string ServiceLocation
        {
            get { return serviceLocation; }
        }

        public void Login()
        {
            try
            {
                if (cookieContainer == null)
                    cookieContainer = new CookieContainer();

                Uri uri = new Uri(baseUri, loginScriptLocation);
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
                request.Method = "POST";
                request.CookieContainer = cookieContainer;

                request.ContentType = "application/x-www-form-urlencoded";
                var requestStream = new StreamWriter(request.GetRequestStream());
                requestStream.Write(ToQueryString("username={0}&passwd={1}&service_type={2}", device.Username, device.Password, serviceID));
                requestStream.Flush();
                requestStream.Close();

                var response = (HttpWebResponse)request.GetResponse();

                //string setCookie = response.Headers["Set-Cookie"];
                //Debug.Assert(!String.IsNullOrEmpty(setCookie));

                //string[] pairs = setCookie.Split(';');
                //NameValueCollection dict = new NameValueCollection();
                //foreach (string pair in pairs)
                //{
                //    int index = pair.IndexOf('=');
                //    string key = pair.Substring(0, index);
                //    string value = pair.Substring(index + 1);
                //    dict.Add(key, value);
                //}
                //string sessionKey = dict["id"];
                //cookieContainer = new CookieContainer();
                //cookieContainer.Add(new Cookie("id", sessionKey, "/", device.Address));

                var responseStream = new StreamReader(response.GetResponseStream());

                string json = responseStream.ReadToEnd();
                LoginResult result = JsonMapper.ToObject<LoginResult>(json);
                result.Test();
                device.IsConnected = true;
            }
            catch (WebException ex)
            {
                throw new CommunicationException("Could not communicate with Station at " + device.Address, ex);
            }
            catch (UriFormatException ex)
            {
                throw new CommunicationException("Invalid location " + device.Address, ex);
            }
        }

        #region Request Functions and overloads

        protected TResult GetAction<TResult>(string action) where TResult : ClientRequestResult
        {
            return GetRequest<TResult>(serviceLocation, "action=" + action);
        }

        protected TResult GetAction<TResult>(string action, string format, params object[] args) where TResult: ClientRequestResult
        {
            return GetRequest<TResult>(serviceLocation, "action=" + action + "&" + ToQueryString(format, args));
        }

        protected TResult PostAction<TResult>(string action, StringDictionary dict) where TResult : ClientRequestResult
        {
            return PostRequest<TResult>(serviceLocation, "action=" + action + "&" + ToQueryString(dict));
        }

        protected TResult PostAction<TResult>(string action, string format, params object[] args) where TResult : ClientRequestResult
        {
            return PostRequest<TResult>(serviceLocation, "action=" + action + "&" + ToQueryString(format, args));
        }

        protected TResult GetRequest<TResult>(string relativeUrl, string format, params object[] args) where TResult : ClientRequestResult
        {
            return GetRequest<TResult>(relativeUrl, ToQueryString(format, args));
        }

        protected TResult PostRequest<TResult>(string relativeUrl, string format, params object[] args) where TResult : ClientRequestResult
        {
            return PostRequest<TResult>(relativeUrl, ToQueryString(format, args));
        }

        protected string PostRequest(string relativeUrl, StringDictionary parameters)
        {
            return PostRequest(relativeUrl, ToQueryString(parameters));
        }

        protected string GetRequest(string relativeUrl, StringDictionary parameters)
        {
            return GetRequest(relativeUrl, ToQueryString(parameters));
        }

        protected TResult PostRequest<TResult>(string relativeUrl, StringDictionary parameters) where TResult : ClientRequestResult
        {
            return PostRequest<TResult>(relativeUrl, ToQueryString(parameters));
        }

        protected TResult GetRequest<TResult>(string relativeUrl, StringDictionary parameters) where TResult : ClientRequestResult
        {
            return GetRequest<TResult>(relativeUrl, ToQueryString(parameters));
        }

        protected string PostRequest(string relativeUrl, string format, params object[] arguments)
        {
            return PostRequest(relativeUrl, ToQueryString(format, arguments));
        }

        protected string GetRequest(string relativeUrl, string format, params object[] arguments)
        {
            return GetRequest(relativeUrl, ToQueryString(format, arguments));
        }

        protected TResult PostRequest<TResult>(string relativeUrl, string data) where TResult : ClientRequestResult
        {
            string result = PostRequest(relativeUrl, data);
            TResult obj;
            try
            {
                obj = JsonMapper.ToObject<TResult>(result);
            }
            catch (JsonException ex)
            {
                throw new CommunicationException("Disk Station returned data in an unexpected format", ex);
            }
            obj.Test();
            return obj;
        }

        protected TResult GetRequest<TResult>(string relativeUrl, string data) where TResult : ClientRequestResult
        {
            string result = GetRequest(relativeUrl, data);
            TResult obj;
            try
            {
                JsonDebugInfo.RemoteMethod = typeof (TResult).Name;
                obj = JsonMapper.ToObject<TResult>(result);
            }
            catch (JsonException ex)
            {
                throw new CommunicationException("Disk Station returned data in an unexpected format", ex);
            }
            obj.Test();
            return obj;
        }

        protected string PostRequest(string relativeUrl, string data)
        {
            Uri uri = new Uri(baseUri, relativeUrl);
            return Request(uri.ToString(), "POST", data);
        }

        protected string GetRequest(string relativeUrl, string data)
        {
            Uri uri = new Uri(baseUri, relativeUrl);
            return Request(uri.ToString(), "GET", data);
        }

        protected string Request(string uri, string method, string data)
        {
            try
            {
                var response = GetResponseStream(uri, method, data);

                var responseStream = new StreamReader(response.GetResponseStream());

                // update device connected state to true (will throw an event when connection was lost)
                device.IsConnected = true;

                return responseStream.ReadToEnd();
            }
            catch (WebException ex)
            {
                device.IsConnected = false;
                throw new CommunicationException("Could not communicate with Station at " + device.Address, ex);
            }
        }

        protected HttpWebResponse GetResponseStream(string uri, string method, string data)
        {
            if (cookieContainer == null)
                Login();

            if (method == "GET")
            {
                if (!string.IsNullOrEmpty(data))
                    uri += "?" + data;
            }

            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
                request.Method = method;
                request.KeepAlive = false; // HACK: Should fix the webexception!

                request.CookieContainer = cookieContainer;

                if (method == "POST")
                {
                    request.ContentType = "application/x-www-form-urlencoded";
                    var requestStream = new StreamWriter(request.GetRequestStream());
                    requestStream.Write(data);
                    requestStream.Flush();
                    requestStream.Close();
                }

                return (HttpWebResponse)request.GetResponse();
            }
            catch (WebException ex)
            {
                device.IsConnected = false;
                throw new CommunicationException("Could not communicate with Station at " + device.Address, ex);
            }
            catch (UriFormatException ex)
            {
                throw new CommunicationException("Invalid location " + device.Address, ex);
            }
        }

        protected static string ToQueryString(StringDictionary parameters)
        {
            StringBuilder sb = new StringBuilder();
            foreach (string key in parameters.Keys)
            {
                sb.Append("&" + key + "=" + HttpUtility.UrlEncode(parameters[key]));
            }
            sb.Remove(0, 1); // remove leading '&'
            return sb.ToString();
        }

        protected static string ToQueryString(string format, params object[] arguments)
        {
            for (int i = 0; i < arguments.Length; i++)
                arguments[i] = HttpUtility.UrlEncode(arguments[i].ToString());

            return String.Format(format, arguments);
        }
        #endregion
    }
}